/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseDfuImpl } from './BaseDfuImpl'
import { DfuBaseService } from './DfuBaseService'
import { ArchiveInputStream } from './internal/ArchiveInputStream'
import ble from '@ohos.bluetooth.ble';
import hilog from '@ohos.hilog';
import zlib from '@ohos.zlib';
import { BusinessError } from '@kit.BasicServicesKit';

export abstract class BaseCustomDfuImpl extends BaseDfuImpl {
  /**
   * Flag indicating whether the init packet has been already transferred or not.
   */
  private mInitPacketInProgress: boolean;
  /**
   * Flag indicating whether the firmware is being transmitted or not.
   */
  public mFirmwareUploadInProgress: boolean;
  /**
   * The number of packets of firmware data to be send before receiving a new Packets
   * receipt notification. 0 disables the packets notifications.
   */
  public mPacketsBeforeNotification: number;
  /**
   * The number of packets sent since last notification.
   */
  private mPacketsSentSinceNotification: number;
  /**
   * <p>
   * Flag set to <code>true</code> when the DFU target had send a notification with status other
   * than success. Setting it to <code>true</code> will abort sending firmware and
   * stop logging notifications (read below for explanation).
   * <p>
   * The onCharacteristicWrite(..) callback is called when Android writes the packet into the
   * outgoing queue, not when it physically sends the data. This means that the service will
   * first put up to N* packets, one by one, to the queue, while in fact the first one is transmitted.
   * In case the DFU target is in an invalid state it will notify Android with a notification
   * 10-03-02 for each packet of firmware that has been sent. After receiving the first such
   * notification, the DFU service will add the reset command to the outgoing queue,
   * but it will still be receiving such notifications until all the data packets are sent.
   * Those notifications should be ignored. This flag will prevent from logging
   * "Notification received..." more than once.
   * <p>
   * Additionally, sometimes after writing the command 6 (OP_CODE_RESET),
   * Android will receive a notification and update the characteristic value with 10-03-02 and
   * the callback for write reset command will log
   * "[DFU] Data written to ..., value (0x): 10-03-02" instead of "...(x0): 06".
   * But this does not matter for the DFU process.
   * <p>
   * N* - Value of Packet Receipt Notification, 12 by default.
   */
  public mRemoteErrorOccurred: boolean;
  public updateCrc: number = 0;

  constructor(service: DfuBaseService) {
    super(service);
    this.TAG = 'BaseCustomDfuImpl';
  }

  protected abstract getControlPointCharacteristicUUID(): string;

  protected abstract getPacketCharacteristicUUID(): string;

  public async uploadFirmwareImage(packetCharacteristic: ble.BLECharacteristic) {
    if (this.mAborted) {
      hilog.error(this.DOMAIN, this.TAG, 'UploadAbortedException');
      throw new Error("UploadAbortedException");
    }

    this.mReceivedData = null;
    this.mError = 0;
    this.mFirmwareUploadInProgress = true;
    this.mPacketsSentSinceNotification = 0;
    hilog.info(this.DOMAIN, this.TAG, 'uploadFirmwareImage enter');
    try {
      let available: number = this.mProgressInfo.getAvailableObjectSizeIsBytes();
      let buffer: Uint8Array = this.mBuffer;
      if (available < buffer.length) {
        buffer = new Uint8Array(available);
      }
      hilog.info(this.DOMAIN, this.TAG, `mProgressInfo available: ${available}, buff: ${buffer.length}`);
      let [resize, rebuffer] = await (this.mFirmwareStream as ArchiveInputStream).readByBuf(buffer);
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE,
        "Sending firmware to characteristic " + packetCharacteristic.characteristicUuid + "...");
      await this.writePacket(this.mGatt, packetCharacteristic, rebuffer, resize);
      hilog.error(this.DOMAIN, this.TAG, 'after writePacket');
      await this.onCharacteristicWrite(packetCharacteristic);
    } catch (e) {
      hilog.error(this.DOMAIN, this.TAG, `uploadFirmwareImage error: ${JSON.stringify(e)}`);
      throw new Error("HEX file not valid: " + DfuBaseService.ERROR_FILE_INVALID);
    }
  }

  // try {
  //   synchronized (mLock) {
  //     while ((mFirmwareUploadInProgress && mReceivedData == null && mConnected && mError == 0) || mPaused)
  //       mLock.wait();
  //   }
  // } catch (final InterruptedException e) {
  //   loge("Sleeping interrupted", e);
  // }

  // if (!mConnected)
  //   throw new DeviceDisconnectedException("Uploading Firmware Image failed: device disconnected");
  // if (mError != 0)
  //   throw new DfuException("Uploading Firmware Image failed", mError);
  // }

  public async onCharacteristicWrite(characteristic: ble.BLECharacteristic) {
    if (characteristic.characteristicUuid == this.getPacketCharacteristicUUID()) {
      if (this.mInitPacketInProgress) {
        // We've got confirmation that the init packet was sent
        this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.characteristicUuid);
        this.mInitPacketInProgress = false;
      } else if (this.mFirmwareUploadInProgress) {
        hilog.info(this.DOMAIN, this.TAG, `onCharacteristicWrite: ${this.mFirmwareUploadInProgress}`);
        this.mPacketsSentSinceNotification++;

        let notificationExpected: boolean = this.mPacketsBeforeNotification > 0 &&
          this.mPacketsSentSinceNotification >= this.mPacketsBeforeNotification;
        let lastPacketTransferred: boolean = this.mProgressInfo.isComplete();
        let lastObjectPacketTransferred: boolean = this.mProgressInfo.isObjectComplete();

        // When a Packet Receipt Notification notification is expected
        // we must not call notifyLock() as the process will resume after notification is received.
        if (notificationExpected) {
          return;
        }

        // In Secure DFU we (usually, depends on the page size and PRN value) do not get any notification after the object is completed,
        // therefore the lock must be notified here to resume the main process.
        if (lastPacketTransferred || lastObjectPacketTransferred) {
          this.mFirmwareUploadInProgress = false;
          // notifyLock();
          return;
        }

        // When neither of them is true, send the next packet
        try {
          //TODO: if need control, add this
          // this.waitIfPaused();
          // The writing might have been aborted (mAborted = true), an error might have occurred.
          // In that case stop sending.
          if (this.mAborted || this.mError != 0 || this.mRemoteErrorOccurred || this.mResetRequestSent) {
            this.mFirmwareUploadInProgress = false;
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload terminated");
            // notifyLock();
            return;
          }

          let available: number = this.mProgressInfo.getAvailableObjectSizeIsBytes();
          let buffer: Uint8Array = this.mBuffer;
          if (available < buffer.length) {
            buffer = new Uint8Array(available);
          }
          hilog.info(this.DOMAIN, this.TAG, `readByBuf: ${available}, ${buffer.length}`);
          let [size, rebuf] = await (this.mFirmwareStream as ArchiveInputStream).readByBuf(buffer);
          hilog.info(this.DOMAIN, this.TAG, `send the next packet: ${size}, ${rebuf.length}`);
          await this.writePacket(this.mGatt, characteristic, rebuf, size);
          await this.onCharacteristicWrite(characteristic);
          return;
        } catch (e) {
          hilog.error(this.DOMAIN, this.TAG, `Invalid : ${JSON.stringify(e)}`);
          this.mError = DfuBaseService.ERROR_FILE_INVALID;
        }
      } else {
        this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.characteristicUuid);
        //TODO: seeyou
        // this.onPacketCharacteristicWrite();
      }
    } else {
      // If the CONTROL POINT characteristic was written just set the flag to true.
      // The main thread will continue its task when notified.
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + characteristic.characteristicUuid);
      this.mRequestCompleted = true;
    }
  }

  private u8ToHStr(uint8Array) {
    return uint8Array.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
  }

  private async writePacket(gatt: ble.GattClientDevice, characteristic: ble.BLECharacteristic,
    buffer: Uint8Array, size: number) {
    let locBuffer: Uint8Array;

    if (size <= 0) {
      // This should never happen
      hilog.error(this.DOMAIN, this.TAG, `size[${size} < 0]`);
      return;
    }
    hilog.info(this.DOMAIN, this.TAG, `writePacket 1`);
    try {
      if (buffer.length != size) {
        // System.arraycopy(buffer, 0, locBuffer, 0, size);
        locBuffer = buffer.slice(0, size);
        hilog.info(this.DOMAIN, this.TAG, `writePacket 12 ${locBuffer.length}`);
      } else {
        locBuffer = buffer;
      }
    } catch(e) {
      hilog.error(this.DOMAIN, this.TAG, `copyWithin catch error: ${size}, ${JSON.stringify(e)}`)
    }

    try {
      // If the PACKET characteristic was written with image data, update counters
      this.mProgressInfo.addBytesSent(size);
      characteristic.characteristicValue = locBuffer.buffer;
      hilog.info(this.DOMAIN, this.TAG, `writePacket: size:${size}, buflen:${locBuffer.length}, buf${this.u8ToHStr(locBuffer)}}`);
      await gatt.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE);
    } catch(e) {
      hilog.error(this.DOMAIN, this.TAG, `writePacket catch error: ${JSON.stringify(e)}`)
    }


    //TODO: if onBLECharacteristicChange not work, assume the write is okay,
    // await this.onCharacteristicWrite(characteristic);
  }

  private async writeInitPacket(characteristic: ble.BLECharacteristic, buffer: Uint8Array, size: number) {
    if (this.mAborted) {
      throw new Error('UploadAbortedException');
    }

    let locBuffer: Uint8Array = buffer;
    if (buffer.length != size) {
      locBuffer = new Uint8Array[size];
      // locBuffer = buffer.copyWithin(0, 0, size-1);
      locBuffer = buffer.slice(0, size-1);
    }
    this.mReceivedData = null;
    this.mError = 0;
    this.mInitPacketInProgress = true;

    hilog.info(this.DOMAIN, this.TAG, "Sending init packet (size: " + locBuffer.length +
      ", value: 0x" + JSON.stringify(locBuffer) + ")");
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "writeInitPacket Writing to characteristic " +
      characteristic.characteristicUuid + " value (0x): " + JSON.stringify(locBuffer));
    // this.mGatt.on("BLECharacteristicChange", async (characteristic: ble.BLECharacteristic) => {
    //   let uuid: string = characteristic.characteristicUuid;
    //   if (uuid === this.getPacketCharacteristicUUID()) {
    //     if (this.mInitPacketInProgress) {
    //       // We've got confirmation that the init packet was sent
    //       this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Data written to " + uuid);
    //       hilog.info(this.DOMAIN, this.TAG, "Data written to " + uuid)
    //       this.mInitPacketInProgress = false;
    //     } else if (this.mFirmwareUploadInProgress) {
    //       this.mPacketsSentSinceNotification++;
    //       let notificationExpected: boolean = this.mPacketsBeforeNotification > 0 &&
    //         this.mPacketsSentSinceNotification >= this.mPacketsBeforeNotification;
    //       let lastPacketTransferred: boolean = this.mProgressInfo.isComplete();
    //       let lastObjectPacketTransferred: boolean = this.mProgressInfo.isObjectComplete();
    //
    //       // When a Packet Receipt Notification notification is expected
    //       // we must not call notifyLock() as the process will resume after notification is received.
    //       if (notificationExpected) {
    //         return;
    //       }
    //
    //       // In Secure DFU we (usually, depends on the page size and PRN value) do not get any notification after the object is completed,
    //       // therefore the lock must be notified here to resume the main process.
    //       if (lastPacketTransferred || lastObjectPacketTransferred) {
    //         this.mFirmwareUploadInProgress = false;
    //         // notifyLock();
    //         return;
    //       }
    //
    //       // When neither of them is true, send the next packet
    //       try {
    //         // waitIfPaused();
    //         // The writing might have been aborted (mAborted = true), an error might have occurred.
    //         // In that case stop sending.
    //         if (this.mAborted || this.mError != 0 || this.mRemoteErrorOccurred || this.mResetRequestSent) {
    //           this.mFirmwareUploadInProgress = false;
    //           this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload terminated");
    //           hilog.info(this.DOMAIN, this.TAG, "Upload terminated");
    //           // notifyLock();
    //           return;
    //         }
    //
    //         let available: number = this.mProgressInfo.getAvailableObjectSizeIsBytes();
    //         let buffer: Uint8Array = this.mBuffer;
    //         if (available < buffer.length) {
    //           buffer = new Uint8Array[available];
    //         }
    //         buffer = this.mFirmwareStream.read(0, buffer.length);
    //         await this.writePacket(this.mGatt, characteristic, buffer, size);
    //         return;
    //       } catch (e) {
    //         hilog.error(this.DOMAIN, this.TAG, "Invalid HEX file");
    //         this.mError = DfuBaseService.ERROR_FILE_INVALID;
    //       }
    //     }
    //   }
    //   this.mGatt.off("BLECharacteristicChange");
    // })
    //TODO: need write gatt
    try {
      characteristic.characteristicValue = locBuffer.buffer;
      await this.mGatt.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE);
      await this.onCharacteristicWrite(characteristic);
      hilog.info(this.DOMAIN, this.TAG, "onCharacteristicWrite finished");
    } catch (err) {
      hilog.error(this.DOMAIN, this.TAG, `Gatt writeCharacteristicValue failed: ${JSON.stringify(err)}`);
    }

    //TODO: this is sync func, must write the slice of init package one by one
    if (!this.mConnected) {
      throw new Error("Unable to write Init DFU Parameters: device disconnected");
    }

    if (this.mError != 0) {
      throw new Error("Unable to write Init DFU Parameters" + JSON.stringify(this.mError));
    }
  }

  public async writeInitData(characteristic: ble.BLECharacteristic, checksum: zlib.Checksum) {
    try {
      let data: Uint8Array = this.mBuffer;
      let size = 0;
      while ((data = this.mInitPacketStream.read(0, data.length)) != null) {
        size = data.length;
        await this.writeInitPacket(characteristic, data, size);
        if (checksum != null) {
          // crc32.update(data, 0, size);
          hilog.info(this.DOMAIN, this.TAG, `checksum: ${JSON.stringify(data)}`);
          let crc: number = await checksum.crc32(0, data.buffer);
          hilog.info(this.DOMAIN, this.TAG, `checksum.crc32: ${JSON.stringify(crc)}`);
          this.updateCrc = crc;
        }
      }
    } catch (e) {
      hilog.error(this.DOMAIN, this.TAG, `Error while reading Init packet file: ${JSON.stringify(e)}`);
      throw new Error("Error while reading Init packet file" + DfuBaseService.ERROR_FILE_ERROR);
    }
  }

  public finalize(forceRefresh: boolean) {
    let keepBond: boolean = false;
    this.mService.refreshDeviceCache(this.mGatt, forceRefresh || !keepBond);

    // Close the device
    this.mService.close(this.mGatt);

    /*
		 * During the update the bonding information on the target device may have been removed.
		 * To create bond with the new application set the EXTRA_RESTORE_BOND extra to true.
		 * In case the bond information is copied to the new application the new bonding is not required.
		 */
    // if (this.mGatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
    //   // final boolean restoreBond = intent.getBooleanExtra(DfuBaseService.EXTRA_RESTORE_BOND, false);
    //   if (this.restoreBond || !this.keepBond) {
    //     // The bond information was lost.
    //     this.removeBond();
    //
    //     // Give some time for removing the bond information. 300 ms was to short,
    //     // let's set it to 2 seconds just to be sure.
    //     this.mService.waitFor(2000);
    //   }
    //
    //   if (this.restoreBond && (this.mFileType & DfuBaseService.TYPE_APPLICATION) > 0) {
    //     // Restore pairing when application was updated.
    //     if (!this.createBond())
    //       hilog.warn(this.DOMAIN, this.TAG, "Creating bond failed");
    //   }
    // }

    /*
     * We need to send PROGRESS_COMPLETED message only when all files has been transmitted.
     * In case you want to send the Soft Device and/or Bootloader and the Application,
     * the service will be started twice: one to send SD+BL, and the second time to send the
     * Application only (using the new Bootloader).
     * In the first case we do not send PROGRESS_COMPLETED notification.
     */
    if (this.mProgressInfo.isLastPart()) {
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_COMPLETED);
    } else {
      /*
       * In case when the SoftDevice has been upgraded, and the application should be send
       * in the following connection, we have to make sure that we know the address the device
       * is advertising with. Depending on the method used to start the DFU bootloader the first time
       * the new Bootloader may advertise with the same address or one incremented by 1.
       * When the buttonless update was used, the bootloader will use the same address as the
       * application. The cached list of services on the Android device should be cleared
       * thanks to the Service Changed characteristic (the fact that it exists if not bonded,
       * or the Service Changed indication on bonded one).
       * In case of forced DFU mode (using a button), the Bootloader does not know whether
       * there was the Service Changed characteristic present in the list of application's
       * services so it must advertise with a different address. The same situation applies
       * when the new Soft Device was uploaded and the old application has been removed in
       * this process.
       *
       * We could have save the fact of jumping as a parameter of the service but Android
       * devices must first scan a device before connecting to it to get the address type.
       * If a device with the address+1 has never been detected before the service could have
       * failed on connection as it would be trying to connect to a public address.
       */

      /*
       * The current service instance has uploaded the Soft Device and/or Bootloader.
       * We need to start another instance that will try to send application only.
       */
      hilog.info(this.DOMAIN, this.TAG, "Starting service that will upload application");
      // final Intent newIntent = new Intent();
      // newIntent.fillIn(intent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_PACKAGE);
      // newIntent.putExtra(DfuBaseService.EXTRA_FILE_MIME_TYPE, DfuBaseService.MIME_TYPE_ZIP); // ensure this is set (e.g. for scripts)
      // newIntent.putExtra(DfuBaseService.EXTRA_FILE_TYPE, DfuBaseService.TYPE_APPLICATION); // set the type to application only
      // newIntent.putExtra(DfuBaseService.EXTRA_PART_CURRENT, mProgressInfo.getCurrentPart() + 1);
      // newIntent.putExtra(DfuBaseService.EXTRA_PARTS_TOTAL, mProgressInfo.getTotalParts());
      this.restartService(/* the bootloader may advertise with different address */ true, this.getDfuServiceUUID());
    }
  }
}